/*
 * Copyright (C) 2012-2013  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.swing.swixml.customization;

import java.awt.Component;
import java.awt.Window;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import nemos.swing.swixml.SwixmlMessage;
import nemos.swing.util.ListenerAdapter;
import nemos.util.log.NemosLogger;

import org.osgi.framework.BundleContext;
import org.swixml.Parser;
import org.swixml.SwingEngine;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * NemosParser : The customized parser
 * 
 * @author Romain DUBOIS
 */
public class NemosParser extends Parser {

    /** The relative location attribute name */
    private static final String RELATIVE_LOCATION = "locationRelativeTo"; //$NON-NLS-1$
    /** The nil string */
    private static final String NULL = "null"; //$NON-NLS-1$
    /** The listener tag */
    private static final String TAG_LISTENER = "listener"; //$NON-NLS-1$
    /** The method tag */
    private static final String TAG_METHOD = "method"; //$NON-NLS-1$
    /** The type attribute */
    private static final String ATT_TYPE = "type"; //$NON-NLS-1$
    /** The prefix attribute */
    private static final String ATT_PREFIX = "prefix"; //$NON-NLS-1$
    /** The name attribute */
    private static final String ATT_NAME = "name"; //$NON-NLS-1$
    /** The add methods prefix */
    private static final String ADD_METHOD_PREFIX = "add"; //$NON-NLS-1$

    /** The bundle context */
    private final BundleContext mContext;
    /** The logger */
    private final NemosLogger mLogger;

    /**
     * Constructor
     * 
     * @param pEngine
     *            The engine
     * @param pContext
     *            The bundle context
     */
    public NemosParser(final SwingEngine<?> pEngine, final BundleContext pContext) {
        super(pEngine);

        mContext = pContext;
        mLogger = new NemosLogger(NemosParser.class);
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.swixml.Parser#getSwing(org.w3c.dom.Element, java.lang.Object)
     */
    @Override
    public Object getSwing(final Element pElement, final Object pObj) throws Exception {

        // Pre-process
        final Collection<Element> lListeners = getListeners(pElement);
        final String lRelativeComponentLocation = getRelativeComponent(pElement);

        // Parse cleaned element normally
        final Object lComponent = super.getSwing(pElement, pObj);

        // Post-process
        if (lComponent != null) {
            if (!lListeners.isEmpty()) {
                createListeners(lListeners, lComponent);
            }
            if (lRelativeComponentLocation != null && lComponent instanceof Window) {
                updateLocation((Window) lComponent, lRelativeComponentLocation);
            }
        }

        return lComponent;
    }

    /**
     * Get the location relative component.
     * 
     * @param pElement
     *            The component description element
     * @return The location component ID
     */
    private String getRelativeComponent(final Element pElement) {
        String lResult = null;

        final NamedNodeMap lAttrs = pElement.getAttributes();
        for (int i = 0; i < lAttrs.getLength(); i++) {
            if (RELATIVE_LOCATION.equalsIgnoreCase(lAttrs.item(i).getLocalName())) {
                lResult = lAttrs.item(i).getNodeValue();
            }
        }
        return lResult;
    }

    /**
     * Update the window relative location.
     * 
     * @param pWindow
     *            The window to locate
     * @param pComponentId
     *            The relative component ID
     */
    private void updateLocation(final Window pWindow, final String pComponentId) {
        if (NULL.equalsIgnoreCase(pComponentId) || pComponentId.trim().isEmpty()) {
            pWindow.setLocationRelativeTo(null);
        } else {
            final Object lComponent = engine.getIdMap().get(pComponentId);
            if (lComponent instanceof Component) {
                pWindow.setLocationRelativeTo((Component) lComponent);
            } else if (lComponent == null) {
                mLogger.warn(SwixmlMessage.WINDOW_RELATIVE_UNKNOWN, pComponentId);
            } else {
                mLogger.warn(SwixmlMessage.WINDOW_RELATIVE_INVALID, pComponentId, lComponent
                        .getClass().getSimpleName());
            }
        }
    }

    /**
     * Create the component listeners.
     * 
     * @param pListeners
     *            The listener descriptions
     * @param pComponent
     *            The listened component
     * @throws Exception
     *             If a listener description is invalid
     */
    private void createListeners(final Collection<Element> pListeners, final Object pComponent)
            throws Exception {
        // Prepare add methods map
        final Map<String, Method> lMethods = new TreeMap<String, Method>(
                String.CASE_INSENSITIVE_ORDER);
        for (final Method lMethod : pComponent.getClass().getMethods()) {
            if (lMethod.getName().startsWith(ADD_METHOD_PREFIX)
                    && lMethod.getParameterTypes().length == 1
                    && lMethod.getName().length() > ADD_METHOD_PREFIX.length()) {
                lMethods.put(lMethod.getName().substring(3), lMethod);
            }
        }

        // Use one adapter for all listeners
        final ListenerAdapter lAdapter = new ListenerAdapter(mContext);
        for (final Element lElement : pListeners) {

            // Find the matching method
            final String lType = lElement.getAttribute(ATT_TYPE);
            final Method lMethod = lMethods.get(lType);
            final String lPrefix = lElement.getAttribute(ATT_PREFIX);

            if (lMethod == null) {
                mLogger.warn(SwixmlMessage.LISTENER_ADD_METHOD_MISSING, lType, pComponent
                        .getClass().getSimpleName());
            } else {

                final Map<String, String> lProperties = new HashMap<String, String>();

                // Check listener type
                final Class<? extends EventListener> lListenerType = cast(
                        lMethod.getParameterTypes()[0], EventListener.class);
                if (lListenerType == null) {
                    mLogger.warn(SwixmlMessage.LISTENER_ADD_METHOD_INCOMPATIBLE, lType,
                            lMethod.toGenericString());
                    continue;
                }

                // Look for methods to adapt
                final NodeList lNodes = lElement.getElementsByTagName(TAG_METHOD);
                for (int j = 0; j < lNodes.getLength(); j++) {
                    final Element lMethodElt = (Element) lNodes.item(j);
                    final String lMethodName = lMethodElt.getAttribute(ATT_NAME);
                    final String lActionName = lMethodElt.getTextContent();

                    if (lMethodName == null || lMethodName.trim().isEmpty()) {
                        mLogger.warn(SwixmlMessage.LISTENER_METHOD_EMPTY, lType);
                    } else if (lActionName == null || lActionName.trim().isEmpty()) {
                        mLogger.warn(SwixmlMessage.LISTENER_ACTION_EMPTY, lType, lMethodName);
                    } else {
                        lProperties.put(lMethodName, lActionName);
                    }
                }

                // Add the listener to the component
                if (!lProperties.isEmpty()) {
                    lAdapter.addListener(lListenerType, lProperties, lPrefix);
                    final Object lProxy = Proxy.newProxyInstance(lListenerType.getClassLoader(),
                            new Class<?>[] { lListenerType }, lAdapter);
                    lMethod.invoke(pComponent, lProxy);
                }
            }
        }
    }

    /**
     * Cast a class into a target one if possible.
     * 
     * @param pCast
     *            The class to cast
     * @param pTarget
     *            The target class
     * @return The casted class
     */
    @SuppressWarnings("unchecked")
    private <T> Class<? extends T> cast(final Class<?> pCast, final Class<T> pTarget) {
        if (pTarget.isAssignableFrom(pCast)) {
            return (Class<? extends T>) pCast;
        }
        return null;
    }

    /**
     * Get the described listeners.
     * 
     * @param pElement
     *            The parsed element
     * @return The listener elements
     */
    private Collection<Element> getListeners(final Element pElement) {
        final Collection<Element> lListeners = new ArrayList<Element>();

        // Look among child elements
        final NodeList lChildren = pElement.getChildNodes();
        for (int i = 0; i < lChildren.getLength(); i++) {

            final Node lChild = lChildren.item(i);
            if (TAG_LISTENER.equalsIgnoreCase(lChild.getLocalName())
                    && lChild.getNodeType() == Node.ELEMENT_NODE) {

                lListeners.add((Element) lChild);
            }
        }

        // Remove to avoid SwiXML to read it
        for (final Element lElement : lListeners) {
            pElement.removeChild(lElement);
        }

        return lListeners;
    }
}
